package com.legsem.jhosync.jmeter;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import junit.framework.TestCase;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public abstract class AbstractTest extends TestCase {

    private Log log = LogFactory.getLog(AbstractTest.class);

    /** Default sync page size (chunks). */
    private static final int DEFAULT_PAGE_SIZE = 2000;

    /** Matches a client ID in a clientcreate reply. */
    public static final Pattern CLIENT_ID_PATTERN = Pattern
            .compile("\\{\"client_id\":\"(\\w+)\"\\}");

    /** Matches a token in a query reply. */
    public static final Pattern TOKEN_PATTERN = Pattern
            .compile("\\{\"token\":\"(\\w*)\"\\}");

    /** Matches the count in a query reply. */
    public static final Pattern COUNT_PATTERN = Pattern
            .compile("\\{\"count\":(\\d*)\\}");

    /** Matches the total count in a query reply. */
    public static final Pattern TOTAL_COUNT_PATTERN = Pattern
            .compile("\\{\"total_count\":(\\d*)\\}");

    /** Matches the progress count in a query reply. */
    public static final Pattern PROGRESS_COUNT_PATTERN = Pattern
            .compile("\\{\"progress_count\":(\\d*)\\}");

    /** Each thread has a separate set of user and client ID. */
    private static AtomicInteger userContextIndex = new AtomicInteger(0);
    private static ThreadLocal < UserContext > userContexts = new ThreadLocal < UserContext >();

    private Properties config;

    public AbstractTest() {
        super();
    }

    /**
     * JMeter calls this constructor passing any text parameter entered in the
     * JMeter configuration.
     * <p/>
     * The text parameter is interpreted as configuration parameters.
     * 
     * @param initParms initial parameters
     */
    public AbstractTest(String initParms) {
        super(initParms);
        init(initParms);
    }

    /**
     * InitParms are properties used to configure a test session.
     * <p/>
     * The content is expected to be similar to a regular properties file only
     * with semi-colon as line separator.
     * 
     * @param initParms initial parameters
     */
    public void init(String initParms) {
        log.info("Initial parameters=" + initParms);
        try {
            config = new Properties();
            byte[] byteArray = initParms.replace(";", "\n").getBytes();
            ByteArrayInputStream baos = new ByteArrayInputStream(byteArray);
            config.load(baos);
            baos.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * Invoked by the JMeter plan once before all tests are run.
     * <p/>
     * Reset the sync engine database to start with a clean slate.
     */
    public void oneTimeSetUp() {
        try {
            UserContext context = new UserContext();
            context.login = getAdminLogin();
            post(context, "/login", "{\"login\":\"" + context.login
                    + "\",\"password\":\"" + getAdminPassword() + "\"}");
            String api_token = post(context, "/api/get_api_token", "{}");
            post(context, "/api/reset", "{\"api_token\":\"" + api_token + "\"}");

        } catch (IOException e) {
            log.error("Database cleaned up failed", e);
        }
        log.info("Database cleaned up");
    }

    /**
     * A generic HTTP POST.
     * 
     * @param path the HTTP path (relative to server_url)
     * @param data the data to POST
     * @return the reply as a string
     * @throws IOException if POST fails
     */
    public String post(String path, String data) throws IOException {
        return post(userContexts.get(), path, data);
    }

    /**
     * A generic HTTP POST.
     * 
     * @param userContext the User/Client requesting POST.
     * @param path the HTTP path (relative to server_url)
     * @param data the data to POST
     * @return the reply as a string
     * @throws IOException if POST fails
     */
    public String post(UserContext userContext, String path, String data)
            throws IOException {
        if (log.isDebugEnabled()) {
            log.debug("about to POST on path [" + path + "] data [" + data
                    + "]");
        }
        PostMethod method = new PostMethod(getServerUrl() + path);
        StringRequestEntity requestEntity = new StringRequestEntity(data,
                "application/json", "UTF-8");
        method.setRequestEntity(requestEntity);
        return exec(userContext, method);
    }

    /**
     * A generic HTTP GET.
     * 
     * @param path the HTTP path (relative to server_url)
     * @return the reply as a string
     * @throws IOException if GET fails
     */
    public String get(String path) throws IOException {
        return get(userContexts.get(), path);
    }

    /**
     * A generic HTTP GET.
     * 
     * @param userContext the User/Client requesting GET.
     * @param path the HTTP path (relative to server_url)
     * @return the reply as a string
     * @throws IOException if GET fails
     */
    public String get(UserContext userContext, String path) throws IOException {
        if (log.isDebugEnabled()) {
            log.debug("about to GET from path [" + path + "]");
        }
        GetMethod method = new GetMethod(getServerUrl() + path);
        return exec(userContext, method);
    }

    /**
     * Generic HTTP method execution.
     * 
     * @param userContext the User/Client requesting GET.
     * @param method the HTTP method
     * @return the results returned
     * @throws IOException if method fails
     */
    protected String exec(UserContext userContext, HttpMethodBase method)
            throws IOException {
        try {
            int rc = userContext.httpClient.executeMethod(method);

            assertEquals(method.getName() + " on path " + method.getPath()
                    + " failed", 200, rc);
            String result = method.getResponseBodyAsString();
            if (log.isDebugEnabled()) {
                log.debug("result from " + method.getName() + " was [" + result
                        + "]");
            }
            return result;
        } finally {
            method.releaseConnection();
        }
    }

    /**
     * Retrieve the server URL
     * 
     * @return example http://192.168.0.112:9292/application
     */
    public String getServerUrl() {
        return getMandatoryConfigParm("server_url");
    }

    /**
     * @return the administrator login (for database cleanup)
     */
    public String getAdminLogin() {
        return getMandatoryConfigParm("admin_login");
    }

    /**
     * @return the administrator password (for database cleanup)
     */
    public String getAdminPassword() {
        String pwd = config.getProperty("admin_password");
        return (pwd == null) ? "" : pwd;
    }

    /**
     * @return the sync source name to work with
     */
    public String getSourceName() {
        return getMandatoryConfigParm("source_name");
    }

    protected String getMandatoryConfigParm(String parmName) {
        String s = config.getProperty(parmName);
        if (s == null) {
            throw new IllegalArgumentException("Parameter " + parmName
                    + " is missing");
        }
        return s;
    }

    /**
     * @return the size of a page of data returned by sync server to client
     */
    public int getPageSize() {
        String s = config.getProperty("page_size");
        return (s == null) ? DEFAULT_PAGE_SIZE : Integer.parseInt(s);
    }

    /**
     * A simulated user and his associated context.
     * 
     */
    public class UserContext {
        public String login;
        public String client_id;
        public String token;
        public HttpClient httpClient = new HttpClient();

        public String toString() {
            return String.format("User:%s, Client ID:%s, token:%s", login,
                    client_id, token);
        }
    }

    /**
     * If this thread does not already have a context, we associate it with a
     * new one.
     * <p/>
     * This means interacting with server to login the user and creating a
     * client ID.
     * <p/>
     * A context is a logged in user with a client ID.
     * 
     * @return a usable context
     * @throws IOException if user context cannot be created
     */
    public UserContext getUserContext() throws IOException {
        synchronized (userContexts) {
            UserContext userContext = userContexts.get();

            if (userContext == null) {
                int i = userContextIndex.getAndIncrement();
                userContext = new UserContext();
                userContext.login = "USER" + i;
                post(userContext, "/clientlogin", "{\"login\":\""
                        + userContext.login
                        + "\",\"password\":\"\",\"remember_me\":1}");
                String createReply = get(userContext, "/clientcreate");
                userContext.client_id = match(CLIENT_ID_PATTERN, createReply);
                userContexts.set(userContext);
            }

            return userContext;
        }
    }

    public static String match(Pattern pattern, String content)
            throws IOException {
        Matcher matcher = pattern.matcher(content);
        matcher.find();
        return matcher.group(1);
    }

}
